===== 第 1 页 ===== 第10章 多线程 · 线程的创建 · 线程的调度 · 多线程通信 · 线程概述 · 线程的生命周期及状态转换 · 多线程同步 · 线程池 ===== 第 2 页 ===== 学习目标 掌握 熟悉 了解 多线程创建的三种方式 线程的生命周期及调度方式 多线程的安全及同步 多线程之间的通信 1 2 线程池的使用 多线程的概念 3 ===== 第 3 页 ===== 线程概述 10.1 线程的创建 10.2 线程的生命周期及状态转换 10.3 线程的调度 10.4 目录 ☞点击查看本小节知识架构 ☞点击查看本小节知识架构 ☞点击查看本小节知识架构 ===== 第 4 页 ===== 多线程同步 10.5 多线程通信 10.6 线程池 10.7 目录 ☞点击查看本小节知识架构 ☞点击查看本小节知识架构 ☞点击查看本小节知识架构 ===== 第 5 页 ===== 10.1 线程概述 10.1.1 10.1.2 进程 线程 返回目录 知识架构 ===== 第 6 页 ===== 10.2 线程的创建 返回目录 知识架构 10.2.1 10.2.2 Driver接口 DriverManager类 10.2.3 Connection接口 10.2.4 10.2.5 Statement接口 PreparedStatement接口 ===== 第 7 页 ===== 10.4 线程的调度 返回目录 知识架构 10.4.1 10.4.2 线程的优先级 线程休眠 10.4.3 线程让步 10.4.4 线程插队 ===== 第 8 页 ===== 10.5 多线程同步 返回目录 知识架构 10.5.1 10.5.2 线程安全 同步代码块 10.5.3 同步方法 10.5.4 10.5.5 同步锁 死锁问题 ===== 第 9 页 ===== 10.6 多线程通信 10.6.1 10.6.2 问题引出 问题如何解决 返回目录 知识架构 ===== 第 10 页 ===== 10.7 线程池 10.7.1 10.7.2 Executor接口实现线程池管理 CompletableFuture类实现线程池管理 返回目录 知识架构 ===== 第 11 页 ===== 10.1 线程概述 定义:在一个操作系统中,每个独立执行的程序都可称之为一个进程,也就是“正在运行的程序”。例如同时运行的QQ、360安全卫士、Eclipse开发工具等。 10.1.1 进程 ===== 第 12 页 ===== 10.1 线程概述 说明: 在多任务操作系统中,表面上看是支持进程并发执行的,例如可以一边听音乐一边聊天,但实际上这些进程并不是在同一时刻运行的。 在计算机中,所有的应用程序都是由CPU执行的,对于一个CPU而言,在某个时间点只能运行一个程序,也就是说只能执行一个进程,操作系统会为每一个进程分配一段有限的CPU使用时间,CPU在这段时间中执行某个进程,然后会在下一段时间切换到另一个进程中去执行。 由于CPU运行速度非常快,能在极短的时间内在不同的进程之间进行切换,所以给人以同时执行多个程序的感觉。 ===== 第 13 页 ===== 10.1 线程概述 定义:在多任务操作系统中,每个运行的程序都是一个进程,用来执行不同的任务,而在一个进程中还可以有多个执行单元同时运行,来同时完成一个或多个程序任务,这些执行单元可以看做程序执行的一条条线索,被称为线程。 注意:操作系统中的每一个进程中都至少存在一个线程,当一个Java程序启动时,就会产生一个进程,该进程中会默认创建一个线程,在这个线程上会运行main()方法中的代码。 10.1.2 线程 ===== 第 14 页 ===== 10.1 线程概述 说明: 单线程都是按照调用顺序依次往下执行,没有出现多段程序代码交替运行的效果,而多线程程序在运行时,每个线程之间都是独立的,它们可以并发执行。 多线程可以充分利用CUP资源,进一步提升程序执行效率。 多线程看似是同时并发执行的,其实不然,它们和进程一样,也是由CPU控制并轮流执行的,只不过CPU运行速度非常快,故而给人同时执行的感觉。 10.1.2 线程——单线程与多线程 ===== 第 15 页 ===== 10.2 线程的创建 创建:Java为多线程开发提供了非常优秀的技术支持,在Java中,可以通过三种方式来实现多线程。 init(FilterConfig filterConfig) 1 继承Thread类,重写run()方法 2 实现Runnable接口,重写run()方法 3 实现Callable接口,重写call()方法,并使用Futrue来获取call()方法的返回结果 ===== 第 16 页 ===== 10.2 线程的创建 说明:Thread类是java.lang包下的一个线程类,用来实现Java多线程。 步骤: 创建一个Thread线程类的子类(子线程),同时重写Thread类的run()方法; 创建该子类的实例对象,并通过调用start()方法启动线程。 10.2.1 Thread类实现多线程 ===== 第 17 页 ===== 10.2 线程的创建 说明:Java只支持类的单继承,如果某个类已经继承了其他父类,就无法再继承Thread类来实现多线程。在这种情况下,就可以考虑通过实现Runnable接口的方式来实现多线程。 步骤: 创建一个Runnable接口的实现类,同时重写接口中的run()方法; 创建Runnable接口的实现类对象; 使用Thread有参构造方法创建线程实例,并将Runnable接口的实现类的实例对象作为参数传入; 调用线程实例的start()方法启动线程。 10.2.2 Runnable接口实现多线程 ===== 第 18 页 ===== 10.2 线程的创建 说明:通过Thread类和Runnable接口实现多线程时,需要重写run()方法,但是由于该方法没有返回值,因此无法从多个线程中获取返回结果。为了解决这个问题,从JDK 5开始,Java提供了一个新的Callable接口,来满足这种既能创建多线程又可以有返回值的需求。 使用:Callable接口实现多线程是通过Thread类的有参构造方法传入Runnable接口类型的参数来实现多线程,不同的是,这里传入的是Runnable接口的子类FutureTask对象作为参数,而FutureTask对象中则封装带有返回值的Callable接口实现类。 10.2.3 Callable接口实现多线程 ===== 第 19 页 ===== 10.2 线程的创建 步骤: 创建一个Callable接口的实现类,同时重写Callable接口的call()方法; 创建Callable接口的实现类对象; 通过FutureTask线程结果处理类的有参构造方法来封装Callable接口实现类对象; 使用参数为FutureTask类对象的Thread有参构造方法创建Thread线程实例; 调用线程实例的start()方法启动线程。 10.2.3 Callable接口实现多线程 ===== 第 20 页 ===== 10.2 线程的创建 说明: Callable接口方式实现的多线程是通过FutureTask类来封装和管理返回结果的,该类的直接父接口是RunnableFuture。 FutureTask本质是Runnable接口和Future接口的实现类,而Future则是JDK 5提供的用来管理线程执行返回结果的。 10.2.3 Callable接口实现多线程——FutureTask继承关系图 ===== 第 21 页 ===== 10.2 线程的创建 10.2.3 Callable接口实现多线程——Future接口方法 方法声明 | 功能描述 boolean cancel(boolean mayInterruptIfRunning) | 用于取消任务,参数mayInterruptIfRunning表示是否允许取消正在执行却没有执行完毕的任务,如果设置true,则表示可以取消正在执行的任务 boolean isCancelled() | 判断任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true boolean isDone() | 判断任务是否已经完成,若任务完成,则返回true V get() | 用于获取执行结果,这个方法会发生阻塞,一直等到任务执行完毕才返回执行结果 V get(long timeout, TimeUnit unit) | 用于在指定时间内获取执行结果,如果在指定时间内,还没获取到结果,就直接返回null ===== 第 22 页 ===== 10.2 线程的创建 通过实现Runnable接口(或者Callable接口)相对于继承Thread类实现多线程来说,有如下显著的好处: 适合多个线程去处理同一个共享资源的情况。 可以避免Java单继承带来的局限性。 说明:事实上,实际开发中大部分的多线程应用都会采用Runnable接口或者Callable接口的方式实现多线程。 。 10.2.4 三种实现多线程方式的对比分析 ===== 第 23 页 ===== 10.2 线程的创建 说明: 对Java程序来说,只要还有一个前台线程在运行,这个进程就不会结束,如果一个进程中只有后台线程运行,这个进程就会结束。 这里提到的前台线程和后台线程是一种相对的概念,新创建的线程默认都是前台线程。 使用:如果某个线程对象在启动之前调用了setDaemon(true)语句,这个线程就变成一个后台线程。 10.2.5 后台线程 ===== 第 24 页 ===== 10.3 线程的生命周期及状态转换 说明: 在Java中,任何对象都有生命周期,线程也不例外,它也有自己的生命周期。 当Thread对象创建完成时,线程的生命周期便开始了。 当线程任务中代码正常执行完毕或者线程抛出一个未捕获的异常(Exception)或者错误(Error)时,线程的生命周期便会结束。 ===== 第 25 页 ===== 10.3 线程的生命周期及状态转换 线程状态转换图: ===== 第 26 页 ===== 10.3 线程的生命周期及状态转换 说明:创建一个线程对象后,该线程对象就处于新建状态,此时它不能运行,和其他Java对象一样,仅仅由JVM为其分配了内存,没有表现出任何线程的动态特征。 NEW(新建状态) ===== 第 27 页 ===== 10.3 线程的生命周期及状态转换 说明:新建状态的线程调用start()方法,就会进入可运行状态。在RUNNABLE状态内部又可细分成两种状态:READY(就绪状态)和RUNNING(运行状态),并且线程可以在这两个状态之间相互转换。 RUNNABLE内部状态转换: 就绪状态:线程对象调用start()方法之后,等待JVM的调度,此时线程并没有运行; 运行状态:线程对象获得JVM调度,如果存在多个CPU,那么允许多个线程并行运行。 RUNNABLE(可运行状态) ===== 第 28 页 ===== 10.3 线程的生命周期及状态转换 说明:运行状态的线程因为某些原因失去CPU的执行权,会进入阻塞状态。阻塞状态的线程只能先进入就绪状态,不能直接进入运行状态。 线程进入阻塞状态的两种情况: 当线程A运行过程中,试图获取同步锁时,却被线程B获取; 当线程运行过程中,发出IO请求时。 BLOCKED(阻塞状态) ===== 第 29 页 ===== 10.3 线程的生命周期及状态转换 说明: 运行状态的线程调用了无时间参数限制的方法后,如wait()、join()等方法,就会转换为等待状态。 等待状态中的线程不能立即争夺CPU使用权,必须等待其他线程执行特定的操作后,才有机会争夺CPU使用权。 例如,调用wait()方法而处于等待状态中的线程,必须等待其他线程调用notify()或者notifyAll()方法唤醒当前等待中的线程; 调用join()方法而处于等待状态中的线程,必须等待其他加入的线程终止。 WAITING(等待状态) ===== 第 30 页 ===== 10.3 线程的生命周期及状态转换 说明: 运行状态中的线程调用了有时间参数限制的方法,如sleep(long millis)、wait(long timeout)、join(long millis)等方法,就会转换为定时等待状态。 定时等待状态中的线程不能立即争夺CPU使用权,必须等待其他相关线程执行完特定的操作或者限时时间结束后,才有机会再次争夺CPU使用权。 例如,调用了wait(long timeout) 方法而处于等待状态中的线程,需要通过其他线程调用notify()或者notifyAll()方法唤醒当前等待中的线程,或者等待限时时间结束后也可以进行状态转换。 TIMED_WAITING(定时等待状态) ===== 第 31 页 ===== 10.3 线程的生命周期及状态转换 说明: 线程的run()方法、call()方法正常执行完毕或者线程抛出一个未捕获的异常(Exception)、错误(Error),线程就进入终止状态。 一旦进入终止状态,线程将不再拥有运行的资格,也不能再转换到其他状态,生命周期结束。 TERMINATED(终止状态) ===== 第 32 页 ===== 10.4 线程的调度 定义:程序中的多个线程是并发执行的,但并不是同一时刻执行,某个线程若想被执行必须要得到CPU的使用权。Java虚拟机会按照特定的机制为程序中的每个线程分配CPU的使用权,这种机制被称作线程的调度。 是指让所有的线程轮流获得CPU的使用权,并且平均分配每个线程占用的CPU时间片。 分时调度模型 是指让可运行池中所有就绪状态的线程争抢CPU的使用权,而优先级高的线程获取CPU执行权的概率大于优先级低的线程。 抢占式调度模型 (JVM默认方式) VS ===== 第 33 页 ===== 10.4 线程的调度 说明: 在应用程序中,要对线程进行调度,最直接的方式就是设置线程的优先级。优先级越高的线程获得CPU执行的机会越大,而优先级越低的线程获得CPU执行的机会越小。 线程的优先级用1~10之间的整数来表示,数字越大优先级越高。 除了可以直接使用数字表示线程的优先级,还可以使用Thread类中提供的三个静态常量表示线程的优先级。 10.4.1 线程的优先级 ===== 第 34 页 ===== 10.4 线程的调度 10.4.1 线程的优先级——Thread类的优先级常量 Thread类的静态常量 | 功能描述 static int MAX_PRIORITY | 表示线程的最高优先级,相当于值10 static int MIN_PRIORITY | 表示线程的最低优先级,相当于值1 static int NORM_PRIORIY | 表示线程的普通优先级,相当于值5 说明:程序在运行期间,处于就绪状态的每个线程都有自己的优先级,例如main线程具有普通优先级。 使用:可以通过Thread类的setPriority(int newPriority)方法对其进行设置,该方法中的参数newPriority接收的是1~10之间的整数或者Thread类的三个静态常量。 ===== 第 35 页 ===== 10.4 线程的调度 说明: 如果想要人为地控制线程执行顺序,使正在执行的线程暂停,将CPU使用权让给其他线程,这时可以使用静态方法sleep(long millis)。 该方法可以让当前正在执行的线程暂停一段时间,进入休眠等待状态,这样其他的线程就可以得到执行的机会。 sleep(long millis)方法会声明抛出InterruptedException异常,因此在调用该方法时应该捕获异常,或者声明抛出该异常。 10.4.2 线程休眠 ===== 第 36 页 ===== 10.4 线程的调度 说明: 线程让步可以通过yield()方法来实现,该方法和sleep(long millis)方法有点类似,都可以让当前正在运行的线程暂停,区别在于yield()方法不会阻塞该线程,它只是将线程转换成就绪状态,让系统的调度器重新调度一次。 当某个线程调用yield()方法之后,与当前线程优先级相同或者更高的线程可以获得执行的机会。 10.4.3 线程让步 ===== 第 37 页 ===== 10.4 线程的调度 说明: 在Thread类中也提供了一个join()方法来实现线程插队功能。 当在某个线程中调用其他线程的join()方法时,调用的线程将被阻塞,直到被join()方法加入的线程执行完成后它才会继续运行。 Thread类中还提供了带有时间参数的线程插队方法join(long millis)。当执行带有时间参数的join(long millis)进行线程插队时,必须等待插入的线程指定时间过后才会继续执行其他线程。 10.4.4 线程插队 ===== 第 38 页 ===== 10.5 多线程同步 为什么要实现多线程同步? 多线程的并发执行可以提高程序的效率,但是,当多个线程去访问同一个资源时,也会引发一些安全问题。例如,当统计一个班级的学生数目时,如果有同学进进出出,则很难统计正确。为了解决这样的问题,需要实现多线程的同步,即限制某个资源在同一时刻只能被一个线程访问。 ===== 第 39 页 ===== 10.5 多线程同步 说明:线程安全问题其实就是由多个线程同时处理共享资源所导致的。要想解决线程安全问题,必须得保证处理共享资源的代码在任意时刻只能有一个线程访问。为此,Java中提供了线程同步机制。 10.5.1 线程安全 同步代码块 同步方法 同步锁 实现线程 同步方式 ===== 第 40 页 ===== 10.5 多线程同步 语法: 注意:lock锁对象可以是任意类型的对象,但多个线程共享的锁对象必须是相同的。锁对象的创建代码不能放到run()方法中,否则每个线程运行到run()方法都会创建一个新对象,这样每个线程都会有一个不同的锁。 10.5.2 同步代码块 synchronized(lock){ // 操作共享资源代码块 ... } lock是一个任意类型的锁对象 Synchronized关键字后大括号{}内包含的就是需要同步操作的共享资源代码块 ===== 第 41 页 ===== 10.5 多线程同步 原理: 当线程执行同步代码块时,首先会检查lock锁对象的标志位; 默认情况下标志位为1,此时线程会执行Synchronized同步代码块,同时将锁对象的标志位置为0; 当一个新的线程执行到这段同步代码块时,由于锁对象的标志位为0,新线程会发生阻塞,等待当前线程执行完同步代码块后; 锁对象的标志位被置为1,新线程才能进入同步代码块执行其中的代码,这样循环往复,直到共享资源被处理完为止。 10.5.2 同步代码块 ===== 第 42 页 ===== 10.5 多线程同步 语法: 说明: 在方法前面也可以使用synchronized关键字来修饰,被修饰的方法为同步方法,它能实现和同步代码块同样的功能。 被synchronized修饰的方法在某一时刻只允许一个线程访问,访问该方法的其他线程都会发生阻塞,直到当前线程访问完毕后,其他线程才有机会执行。 10.5.3 同步方法 [修饰符] synchronized 返回值类型 方法名([参数1,……]){} ===== 第 43 页 ===== 10.5 多线程同步 注意: 同步方法也有锁,它的锁就是当前调用该方法的对象,就是this指向的对象。 Java中静态方法的锁是该方法所在类的class对象,该对象可以直接类名.class的方式获取。 同步代码块和同步方法解决多线程问题有好处也有弊端。同步解决了多个线程同时访问共享数据时的线程安全问题,只要加上同一个锁,在同一时间内只能有一条线程执行,但是线程在执行同步代码时每次都会判断锁的状态,非常消耗资源,效率较低。 10.5.3 同步方法 ===== 第 44 页 ===== 10.5 多线程同步 问题:synchronized同步代码块和同步方法使用一种封闭式的锁机制,使用起来非常简单,也能够解决线程同步过程中出现的线程安全问题,但也有一些限制,例如它无法中断一个正在等候获得锁的线程,也无法通过轮询得到锁,如果不想等下去,也就没法得到锁。 解决:从JDK 5开始,Java增加了一个功能更强大的Lock锁。Lock锁与synchronized隐式锁在功能上基本相同,其最大的优势在于Lock锁可以让某个线程在持续获取同步锁失败后返回,不再继续等待,另外Lock锁在使用时也更加灵活。 10.5.4 同步锁 ===== 第 45 页 ===== 10.5 多线程同步 注意: ReentrantLock类是Lock锁接口的实现类,也是常用的同步锁,在该同步锁中除了lock()方法和unlock()方法外,还提供了一些其他同步锁操作的方法,例如tryLock()方法可以判断某个线程锁是否可用。 在使用Lock同步锁时,可以根据需要在不同代码位置灵活的上锁和解锁,为了保证所有情况下都能正常解锁以确保其他线程可以执行,通常情况下会在finally{}代码块中调用unlock()方法来解锁。 10.5.4 同步锁 ===== 第 46 页 ===== 10.5 多线程同步 定义:两个线程在运行时都在等待对方的锁,这样便造成了程序的停滞,这种现象称为死锁。 避免死锁方式: 加锁顺序(线程按照一定的顺序加锁) 加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁) 死锁检测 10.5.5 死锁问题 ===== 第 47 页 ===== 10.6 多线程通信 为什么需要多线程通信? 现代社会崇尚合作精神,分工合作在日常生活和工作中无处不在。举个简单的例子,例如一条生产线的上下两个工序,它们必须以规定的速率完成各自的工作,才能保证产品在流水线中顺利的流转。如果下工序过慢,会造成产品在两道工序之间的积压,如果上工序过慢,会造成下工序长时间无事可做。在多线程的程序中,上下工序可以看作两个线程,这两个线程之间需要协同完成工作,就需要线程之间进行通信。 ===== 第 48 页 ===== 10.6 多线程通信 思路:如果想解决上述线程执行不一致的问题,就需要控制多个线程按照一定的顺序轮流执行,此时就需要让线程间进行通信,保证线程任务的协调进行。 解决:为此,Java在Object类中提供了wait()、notify()、notifyAll()等方法用于解决线程间的通信问题,因为Java中所有类都是Object类的子类或间接子类,因此任何类的实例对象都可以直接使用这些方法。 10.6.2 问题如何解决 ===== 第 49 页 ===== 10.6 多线程通信 说明:其中wait()方法用于使当前线程进入等待状态,notify()和notifyAll()方法用于唤醒当前处于等待状态的线程。 注意:wait()、notify()和notifyAll()方法的调用者都应该是同步锁对象,如果这三个方法的调用者不是同步锁对象,Java虚拟机就会抛出IllegalMonitorStateException异常。 10.6.2 问题如何解决——线程通信的常用方法 方法声明 | 功能描述 void wait() | 使当前线程放弃同步锁并进入等待,直到其他线程进入此同步锁,并调用notify()或notifyAll()方法唤醒该线程为止 void notify() | 唤醒此同步锁上等待的第一个调用wait()方法的线程 void notifyAll() | 唤醒此同步锁上调用wait()方法的所有线程 ===== 第 50 页 ===== 10.7 线程池 为什么需要线程池技术? 在前面几个小节中讲解了基础的多线程实现,对于基本的任务来说是足够的,但是对于更复杂的任务来说这种频繁手动式的创建、管理线程显然是不可取的,因为线程对象使用了大量的内存,在大规模应用程序中,创建、分配和释放多线程对象会产生大量内存管理开销。为此,可以考虑使用Java提供的线程池来创建多线程,进一步优化线程管理。 ===== 第 51 页 ===== 10.7 线程池 步骤: 创建一个实现Runnable接口或者Callable接口的实现类,同时重写run()或者call()方法; 创建Runnable接口或者Callable接口的实现类对象; 使用Executors线程执行器类创建线程池; 使用ExecutorService执行器服务类的submit()方法将Runnable接口或者Callable接口的实现类对象提交到线程池进行管理; 线程任务执行完成后,可以使用shutdown()方法关闭线程池。 10.7.1 Executor接口实现线程池管理 ===== 第 52 页 ===== 10.7 线程池 10.7.1 Executor接口实现线程池管理——Executors创建线程池的方法 方法声明 | 功能描述 ExecutorService newCachedThreadPool() | 创建一个可扩展线程池的执行器。这个线程池执行器适用于启动许多短期任务的应用程序 ExecutorService newFixedThreadPool( int nThreads) | 创建一个固定线程数量线程池的执行器。这种线程池执行器可以很好的控制多线程任务,也不会导致由于响应过多导致的程序崩溃 ExecutorService newSingleThreadExecutor() | 在特殊需求下创建一个只执行一个任务的单个线程 ScheduledExecutorService newScheduledThreadPool(int corePoolSize) | 创建一个定长线程池,支持定时及周期性任务执行 ===== 第 53 页 ===== 10.7 线程池 说明:在使用Callable接口实现多线程时,会用到FutureTask类对线程执行结果进行管理和获取,由于该类在获取结果时是通过阻塞或者轮询的方式,违背多线程编程的初衷且耗费过多资源。 解决:JDK 8中新增了一个强大的函数式异步编程辅助类CompletableFuture,该类同时实现了Future接口和CompletionStage接口(Java 8中新增的一个线程任务完成结果接口),并对Future进行了强大的扩展,简化异步编程的复杂性。 10.7.2 CompletableFuture类实现线程池管理 ===== 第 54 页 ===== 10.7 线程池 CompletableFuture对象创建的四个静态方法: 10.7.2 CompletableFuture类实现线程池管理 方法声明 | 功能描述 static CompletableFuture runAsync(Runnable runnable) | 以Runnable函数式接口类型为参数,并使用ForkJoinPool.commonPool()作为它的线程池执行异步代码获取CompletableFuture计算结果为空的对象 static CompletableFuture runAsync(Runnable runnable, Executor executor) | 以Runnable函数式接口类型为参数,并传入指定的线程池执行器executor来获取CompletableFuture计算结果为空的对象 static CompletableFuture supplyAsync(Supplier supplier) | 以Supplier函数式接口类型为参数,并使用ForkJoinPool.commonPool()作为它的线程池执行异步代码获取CompletableFuture计算结果非空的对象 static CompletableFuture supplyAsync(Supplier supplier, Executor executor) | 以Supplier函数式接口类型为参数,并传入指定的线程池执行器executor来获取CompletableFuture计算结果非空的对象 ===== 第 55 页 ===== 章主要讲解了线程的创建、线程的生命周期和状态转换、线程的调度、线程同步、线程通信以及线程池等方面的知识。这里讲解的几乎每一个知识点都是多线程的重点内容,需要认真学习和体会。 10.8 本章小结 ===== 第 56 页 =====